#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
任务调度器核心模块
负责管理定时任务的调度、执行和监控
"""

import threading
import time
import logging
import subprocess
import os
import signal
from datetime import datetime, timedelta
from typing import Dict, List, Callable, Optional, Any
from enum import Enum
from concurrent.futures import ThreadPoolExecutor
import json
import shutil


# 导入模块，支持相对导入和绝对导入
try:
    from .cron_parser import CronParser
except ImportError:
    # 如果直接运行文件，尝试绝对导入
    try:
        from cron_parser import CronParser
    except ImportError as e:
        print(f"导入错误: {e}", file=__import__('sys').stderr)
        print("请确保项目结构正确", file=__import__('sys').stderr)
        __import__('sys').exit(1)


class DailyRotatingFileHandler(logging.FileHandler):
    """按天轮转的日志文件处理器，保留指定天数的日志"""
    
    def __init__(self, filename, encoding='utf-8', delay=False, backup_count=7, log_dir=None):
        """
        初始化日志处理器
        
        Args:
            filename: 日志文件名基础（不含日期）
            encoding: 文件编码
            delay: 是否延迟创建文件
            backup_count: 保留的日志文件天数
            log_dir: 日志目录路径
        """
        self.base_filename = filename
        self.backup_count = backup_count
        self.log_dir = log_dir
        self.current_date = datetime.now().strftime("%Y-%m-%d")
        
        # 处理日志目录
        filename_with_date = self._get_filename_with_date(self.current_date)
        
        # 确保日志目录存在
        log_dir_path = os.path.dirname(filename_with_date)
        if log_dir_path:
            os.makedirs(log_dir_path, exist_ok=True)
        
        super().__init__(filename_with_date, mode='a', encoding=encoding, delay=delay)
        self._cleanup_old_logs()
    
    def _get_filename_with_date(self, date_str):
        """获取带日期的日志文件名"""
        filename_no_ext, ext = os.path.splitext(os.path.basename(self.base_filename))
        if self.log_dir:
            # 如果指定了日志目录，则在该目录下创建日志文件
            dated_filename = os.path.join(self.log_dir, f"{filename_no_ext}_{date_str}{ext}")
        else:
            # 否则保持原来的行为，在原文件目录下创建
            base_dir = os.path.dirname(self.base_filename)
            if base_dir:
                dated_filename = os.path.join(base_dir, f"{filename_no_ext}_{date_str}{ext}")
            else:
                dated_filename = f"{filename_no_ext}_{date_str}{ext}"
        return dated_filename
    
    def _cleanup_old_logs(self):
        """清理过期的日志文件"""
        try:
            # 确定日志目录
            if self.log_dir:
                log_dir = self.log_dir
            else:
                log_dir = os.path.dirname(self.base_filename) or "."
            
            if not os.path.exists(log_dir):
                return
            
            # 获取基础文件名（不含路径和扩展名）
            base_name = os.path.basename(self.base_filename)
            base_name_no_ext = os.path.splitext(base_name)[0]
            
            # 获取所有相关日志文件
            log_files = []
            for file in os.listdir(log_dir):
                if file.startswith(f"{base_name_no_ext}_") and file.endswith(".log"):
                    log_files.append(file)
            
            # 计算保留日期
            cutoff_date = datetime.now() - timedelta(days=self.backup_count)
            
            # 删除过期的日志文件
            for log_file in log_files:
                try:
                    # 提取日期部分
                    date_part = log_file[len(base_name_no_ext)+1:-4]  # 去掉前缀和后缀
                    file_date = datetime.strptime(date_part, "%Y-%m-%d")
                    
                    if file_date < cutoff_date:
                        os.remove(os.path.join(log_dir, log_file))
                        print(f"已删除过期日志文件: {log_file}")
                except (ValueError, OSError):
                    # 解析日期失败或删除失败，跳过
                    pass
        except Exception as e:
            # 避免日志清理过程中的异常影响主程序
            pass
    
    def emit(self, record):
        """发出日志记录"""
        # 检查是否需要切换到新的日志文件
        current_date = datetime.now().strftime("%Y-%m-%d")
        if current_date != self.current_date:
            # 关闭当前文件
            self.close()
            
            # 更新日期
            self.current_date = current_date
            
            # 创建新的日志文件
            self.baseFilename = self._get_filename_with_date(self.current_date)
            
            # 清理过期日志
            self._cleanup_old_logs()
        
        # 调用父类方法记录日志
        super().emit(record)


class TaskStatus(Enum):
    """任务状态枚举"""
    PENDING = "pending"      # 等待执行
    RUNNING = "running"      # 正在执行
    COMPLETED = "completed"  # 执行完成
    FAILED = "failed"        # 执行失败
    DISABLED = "disabled"    # 已禁用


class TaskResult:
    """任务执行结果"""

    def __init__(self, task_id: str, status: TaskStatus, start_time: datetime,
                 end_time: Optional[datetime] = None, output: str = "",
                 error: str = "", exit_code: Optional[int] = None):
        self.task_id = task_id
        self.status = status
        self.start_time = start_time
        self.end_time = end_time
        self.output = output
        self.error = error
        self.exit_code = exit_code

    def to_dict(self) -> Dict[str, Any]:
        """转换为字典格式"""
        return {
            "task_id": self.task_id,
            "status": self.status.value,
            "start_time": self.start_time.isoformat(),
            "end_time": self.end_time.isoformat() if self.end_time else None,
            "output": self.output,
            "error": self.error,
            "exit_code": self.exit_code
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'TaskResult':
        """从字典创建实例"""
        return cls(
            task_id=data["task_id"],
            status=TaskStatus(data["status"]),
            start_time=datetime.fromisoformat(data["start_time"]),
            end_time=datetime.fromisoformat(data["end_time"]) if data.get("end_time") else None,
            output=data.get("output", ""),
            error=data.get("error", ""),
            exit_code=data.get("exit_code")
        )


class ScheduledTask:
    """定时任务类"""

    def __init__(self, task_id: str, name: str, cron_expression: str,
                 command: str, enabled: bool = True, description: str = "",
                 working_dir: str = "", environment: Optional[Dict[str, str]] = None,
                 timeout: Optional[int] = None, max_retries: int = 0):
        """
        初始化定时任务

        Args:
            task_id: 任务唯一标识
            name: 任务名称
            cron_expression: cron表达式
            command: 要执行的命令
            enabled: 是否启用
            description: 任务描述
            working_dir: 工作目录
            environment: 环境变量
            timeout: 超时时间（秒）
            max_retries: 最大重试次数
        """
        self.task_id = task_id
        self.name = name
        self.cron_expression = cron_expression
        self.command = command
        self.enabled = enabled
        self.description = description
        self.working_dir = working_dir or os.getcwd()
        self.environment = environment or {}
        self.timeout = timeout
        self.max_retries = max_retries

        # 运行时状态
        self.status = TaskStatus.PENDING
        self.last_run_time: Optional[datetime] = None
        self.next_run_time: Optional[datetime] = None
        self.retry_count = 0
        self.cron_parser = CronParser(cron_expression)

        # 结果历史
        self.results: List[TaskResult] = []

    def update_next_run_time(self, from_time: Optional[datetime] = None):
        """更新下次运行时间"""
        if not self.enabled:
            self.next_run_time = None
            return

        try:
            self.next_run_time = self.cron_parser.get_next_run_time(from_time)
        except Exception as e:
            logging.error(f"计算任务 {self.task_id} 下次运行时间失败: {e}")
            self.next_run_time = None

    def should_run(self, current_time: datetime) -> bool:
        """检查任务是否应该在当前时间运行"""
        if not self.enabled or not self.next_run_time:
            return False

        return current_time >= self.next_run_time

    def to_dict(self) -> Dict[str, Any]:
        """转换为字典格式"""
        return {
            "task_id": self.task_id,
            "name": self.name,
            "cron_expression": self.cron_expression,
            "command": self.command,
            "enabled": self.enabled,
            "description": self.description,
            "working_dir": self.working_dir,
            "environment": self.environment,
            "timeout": self.timeout,
            "max_retries": self.max_retries,
            "last_run_time": self.last_run_time.isoformat() if self.last_run_time else None,
            "next_run_time": self.next_run_time.isoformat() if self.next_run_time else None,
            "status": self.status.value,
            "retry_count": self.retry_count
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'ScheduledTask':
        """从字典创建实例"""
        task = cls(
            task_id=data["task_id"],
            name=data["name"],
            cron_expression=data["cron_expression"],
            command=data["command"],
            enabled=data.get("enabled", True),
            description=data.get("description", ""),
            working_dir=data.get("working_dir", ""),
            environment=data.get("environment", {}),
            timeout=data.get("timeout"),
            max_retries=data.get("max_retries", 0)
        )

        # 恢复运行时状态
        if data.get("last_run_time"):
            task.last_run_time = datetime.fromisoformat(data["last_run_time"])
        if data.get("next_run_time"):
            task.next_run_time = datetime.fromisoformat(data["next_run_time"])
        task.status = TaskStatus(data.get("status", "pending"))
        task.retry_count = data.get("retry_count", 0)

        return task


class TaskScheduler:
    """任务调度器"""

    def __init__(self, max_workers: int = 5, log_file: str = "scheduler.log", log_dir: str = None):
        """
        初始化任务调度器

        Args:
            max_workers: 最大并发任务数
            log_file: 日志文件路径
            log_dir: 日志目录路径
        """
        self.tasks: Dict[str, ScheduledTask] = {}
        self.running = False
        self.thread: Optional[threading.Thread] = None
        self.executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="TaskExecutor")

        # 设置日志
        self._setup_logging(log_file, log_dir)

        # 停止事件
        self.stop_event = threading.Event()
        
        # 记录上次检查时间，用于检测系统休眠后的时间跳跃
        self.last_check_time: Optional[datetime] = None

        logging.info("任务调度器初始化完成")

    def _setup_logging(self, log_file: str, log_dir: str = None):
        """设置日志配置"""
        # 先检查日志文件路径是否为绝对路径
        if not os.path.isabs(log_file):
            # 如果是相对路径，则相对于当前工作目录
            log_file = os.path.join(os.getcwd(), log_file)
        
        # 确保日志目录存在
        if log_dir:
            # 如果指定了日志目录，则使用该目录
            if not os.path.isabs(log_dir):
                # 日志目录为相对路径，相对于当前工作目录
                log_dir = os.path.join(os.getcwd(), log_dir)
            os.makedirs(log_dir, exist_ok=True)
        else:
            # 否则使用日志文件所在目录
            log_dir = os.path.dirname(log_file)
            if log_dir:
                os.makedirs(log_dir, exist_ok=True)
        
        # 重新配置日志系统，使用force=True确保重新配置
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s [%(levelname)s] %(message)s',
            handlers=[
                DailyRotatingFileHandler(log_file, encoding='utf-8', backup_count=7, log_dir=log_dir),
                logging.StreamHandler()
            ],
            force=True  # 强制重新配置日志
        )

    def add_task(self, task: ScheduledTask):
        """添加任务"""
        if task.task_id in self.tasks:
            raise ValueError(f"任务ID {task.task_id} 已存在")

        self.tasks[task.task_id] = task
        task.update_next_run_time()
        logging.info(f"添加任务: {task.name} ({task.task_id})")

    def remove_task(self, task_id: str):
        """移除任务"""
        if task_id not in self.tasks:
            raise ValueError(f"任务ID {task_id} 不存在")

        task = self.tasks[task_id]
        del self.tasks[task_id]
        logging.info(f"移除任务: {task.name} ({task_id})")

    def update_task(self, task: ScheduledTask):
        """更新任务"""
        if task.task_id not in self.tasks:
            raise ValueError(f"任务ID {task.task_id} 不存在")

        old_task = self.tasks[task.task_id]
        self.tasks[task.task_id] = task
        task.update_next_run_time()
        logging.info(f"更新任务: {old_task.name} -> {task.name} ({task.task_id})")

    def enable_task(self, task_id: str):
        """启用任务"""
        if task_id not in self.tasks:
            raise ValueError(f"任务ID {task_id} 不存在")

        task = self.tasks[task_id]
        if not task.enabled:
            task.enabled = True
            task.update_next_run_time()
            logging.info(f"启用任务: {task.name} ({task_id})")

    def disable_task(self, task_id: str):
        """禁用任务"""
        if task_id not in self.tasks:
            raise ValueError(f"任务ID {task_id} 不存在")

        task = self.tasks[task_id]
        if task.enabled:
            task.enabled = False
            task.next_run_time = None
            logging.info(f"禁用任务: {task.name} ({task_id})")

    def get_task(self, task_id: str) -> Optional[ScheduledTask]:
        """获取任务"""
        return self.tasks.get(task_id)

    def list_tasks(self) -> List[ScheduledTask]:
        """列出所有任务"""
        return list(self.tasks.values())

    def start(self):
        """启动调度器"""
        if self.running:
            logging.warning("调度器已在运行")
            return

        self.running = True
        self.stop_event.clear()
        self.thread = threading.Thread(target=self._scheduler_loop, name="TaskScheduler")
        self.thread.daemon = True
        self.thread.start()
        logging.info("任务调度器已启动")

    def stop(self):
        """停止调度器"""
        if not self.running:
            return

        self.running = False
        self.stop_event.set()

        # 等待调度线程结束
        if self.thread and self.thread.is_alive():
            self.thread.join(timeout=5)

        # 关闭线程池
        self.executor.shutdown(wait=True)
        logging.info("任务调度器已停止")

    def _scheduler_loop(self):
        """调度器主循环"""
        logging.info("调度器主循环开始")

        while not self.stop_event.is_set():
            try:
                current_time = datetime.now()
                
                # 检测系统是否从休眠中恢复（时间跳跃超过2分钟）
                if self.last_check_time and (current_time - self.last_check_time) > timedelta(minutes=2):
                    logging.info(f"检测到系统可能从休眠中恢复，时间跳跃: {self.last_check_time} -> {current_time}")
                    # 重新计算所有任务的下次运行时间
                    for task in self.tasks.values():
                        if task.enabled:
                            task.update_next_run_time()
                
                self.last_check_time = current_time

                # 检查需要执行的任务
                for task in self.tasks.values():
                    if task.should_run(current_time):
                        self._execute_task(task)

                # 每秒检查一次
                self.stop_event.wait(1)

            except Exception as e:
                logging.error(f"调度器循环错误: {e}")
                time.sleep(1)

        logging.info("调度器主循环结束")

    def _execute_task(self, task: ScheduledTask):
        """执行任务"""
        if task.status == TaskStatus.RUNNING:
            logging.warning(f"任务 {task.task_id} 已在运行中，跳过本次执行")
            return

        # 提交任务到线程池执行
        future = self.executor.submit(self._run_task_async, task)
        logging.info(f"提交任务 {task.task_id} 到执行队列")

    def _run_task_async(self, task: ScheduledTask):
        """异步执行任务"""
        try:
            task.status = TaskStatus.RUNNING
            task.last_run_time = datetime.now()
            task.retry_count = 0

            logging.info(f"开始执行任务: {task.name} ({task.task_id})")

            # 创建任务结果对象
            result = TaskResult(task.task_id, TaskStatus.RUNNING, task.last_run_time)

            # 执行命令
            success = self._execute_command(task, result)

            # 更新任务状态
            if success:
                task.status = TaskStatus.COMPLETED
                result.status = TaskStatus.COMPLETED
                logging.info(f"任务执行成功: {task.name} ({task.task_id})")
            else:
                task.status = TaskStatus.FAILED
                result.status = TaskStatus.FAILED
                logging.error(f"任务执行失败: {task.name} ({task.task_id})")

                # 处理重试
                if task.retry_count < task.max_retries:
                    task.retry_count += 1
                    logging.info(f"任务 {task.task_id} 将在5秒后重试 ({task.retry_count}/{task.max_retries})")
                    time.sleep(5)
                    self._execute_task(task)  # 递归重试
                    return

            # 完成结果记录
            result.end_time = datetime.now()
            task.results.append(result)

            # 保留最近10个结果
            if len(task.results) > 10:
                task.results = task.results[-10:]

            # 更新下次运行时间
            task.update_next_run_time(result.end_time)

        except Exception as e:
            logging.error(f"任务 {task.task_id} 执行异常: {e}")
            task.status = TaskStatus.FAILED

    def _execute_command(self, task: ScheduledTask, result: TaskResult) -> bool:
        """执行命令"""
        try:
            # 设置环境变量
            env = os.environ.copy()
            env.update(task.environment)

            # 创建进程 - 使用字节模式以避免编码问题
            process = subprocess.Popen(
                task.command,
                shell=True,
                cwd=task.working_dir,
                env=env,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE
            )

            # 等待执行完成
            try:
                if task.timeout:
                    stdout_bytes, stderr_bytes = process.communicate(timeout=task.timeout)
                else:
                    stdout_bytes, stderr_bytes = process.communicate()

                result.exit_code = process.returncode

                # 安全地解码输出，避免编码错误
                try:
                    result.output = stdout_bytes.decode('utf-8', errors='replace') if stdout_bytes else ""
                    result.error = stderr_bytes.decode('utf-8', errors='replace') if stderr_bytes else ""
                except UnicodeDecodeError:
                    # 如果UTF-8解码失败，尝试其他编码
                    try:
                        result.output = stdout_bytes.decode('gbk', errors='replace') if stdout_bytes else ""
                        result.error = stderr_bytes.decode('gbk', errors='replace') if stderr_bytes else ""
                    except UnicodeDecodeError:
                        # 最后尝试忽略错误
                        result.output = stdout_bytes.decode('utf-8', errors='ignore') if stdout_bytes else ""
                        result.error = stderr_bytes.decode('gbk', errors='ignore') if stderr_bytes else ""

                return process.returncode == 0

            except subprocess.TimeoutExpired:
                logging.warning(f"任务 {task.task_id} 执行超时 ({task.timeout}秒)，终止进程")
                process.terminate()

                # 等待进程终止
                try:
                    process.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    process.kill()

                result.error = f"执行超时 ({task.timeout}秒)"
                result.exit_code = -1
                return False

        except Exception as e:
            result.error = str(e)
            result.exit_code = -1
            return False

    def get_task_status(self, task_id: str) -> Optional[Dict[str, Any]]:
        """获取任务状态"""
        task = self.get_task(task_id)
        if not task:
            return None

        return {
            "task_id": task.task_id,
            "name": task.name,
            "status": task.status.value,
            "enabled": task.enabled,
            "last_run_time": task.last_run_time.isoformat() if task.last_run_time else None,
            "next_run_time": task.next_run_time.isoformat() if task.next_run_time else None,
            "retry_count": task.retry_count,
            "results": [r.to_dict() for r in task.results[-5:]]  # 最近5个结果
        }

    def save_state(self, file_path: str):
        """保存调度器状态"""
        try:
            state = {
                "tasks": {task_id: task.to_dict() for task_id, task in self.tasks.items()},
                "timestamp": datetime.now().isoformat()
            }

            with open(file_path, 'w', encoding='utf-8') as f:
                json.dump(state, f, indent=2, ensure_ascii=False)

            logging.info(f"调度器状态已保存到: {file_path}")

        except Exception as e:
            logging.error(f"保存调度器状态失败: {e}")

    def load_state(self, file_path: str):
        """加载调度器状态"""
        try:
            if not os.path.exists(file_path):
                logging.info(f"状态文件不存在: {file_path}")
                return

            with open(file_path, 'r', encoding='utf-8') as f:
                state = json.load(f)

            tasks_data = state.get("tasks", {})
            for task_id, task_data in tasks_data.items():
                try:
                    task = ScheduledTask.from_dict(task_data)
                    self.tasks[task_id] = task
                    logging.info(f"加载任务: {task.name} ({task_id})")
                except Exception as e:
                    logging.error(f"加载任务 {task_id} 失败: {e}")

            logging.info(f"调度器状态已从 {file_path} 加载")

        except Exception as e:
            logging.error(f"加载调度器状态失败: {e}")


if __name__ == "__main__":
    # 测试代码
    scheduler = TaskScheduler()

    # 添加测试任务
    task1 = ScheduledTask(
        task_id="test1",
        name="每分钟任务",
        cron_expression="* * * * * *",
        command="echo 'Hello World'",
        description="测试任务"
    )

    task2 = ScheduledTask(
        task_id="test2",
        name="每5秒任务",
        cron_expression="*/5 * * * * *",
        command="python -c \"print('每5秒执行一次')\"",
        description="另一个测试任务"
    )

    scheduler.add_task(task1)
    scheduler.add_task(task2)

    print("启动调度器测试 (10秒后自动停止)...")
    scheduler.start()

    try:
        time.sleep(10)
    finally:
        print("停止调度器...")
        scheduler.stop()
        print("测试完成")